/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

****/
package org.activequant.util.charting;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

import org.activequant.core.types.Tuple;
import org.jfree.chart.ChartFrame;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.general.Series;
import org.jfree.data.xy.XYDataset;

/**
 * Base for the combined charts. This class is responsible for implementing needed interfaces, and other common housekeeping.<br/>
 * An abstract class. CombinedDomainChartBase&lt;T extends Comparable&lt;T&gt;&gt; implements ICombinedDomainChart&lt;T&gt;.<br/>
 * Holds the following associated variables:
 * <ul>
 * <li>plot(CombinedDomainXYPlot)</li>
 * <li>chart(JFreeChart)</li>
 * <li>frame(JFrame)</li>
 * <li>seriesList(List&lt;Series&gt;)</li>
 * <li>latestX(AtomicReference&lt;T&gt;)</li>
 * <li>isCleanupScheduled(AtomicBoolean)</li>
 * </ul>
 * Defines the following abstract methods:
 * <ul>
 * <li>XYDataset createDataset()</li>
 * <li>Series createSeries(XYDataset dataset, String legend)</li>
 * <li>void cleanSeries(Series series, T max)</li>
 * <li>addToSeries(Series series, T x, double y)</li>
 * </ul>
 * <pr>
 * <b>History:</b><br>
 *  - [30.12.2007] Created (Mike Kroutikov)<br>
 *
 *  @author Mike Kroutikov
 */
public abstract class CombinedDomainChartBase<T extends Comparable<T>> implements ICombinedDomainChart<T> {
	/**
	 * private final CombinedDomainXYPlot plot;
	 */
	private final CombinedDomainXYPlot plot;
	/**
	 * private final JFreeChart chart;
	 */
	private final JFreeChart chart;
	/**
	 * private final JFrame frame;
	 */
	private final JFrame frame;
	/**
	 * private XYPlot annotatedXYPlot;<br/>
	 * where we want annotations to go to
	 */
	private XYPlot annotatedXYPlot;
	/**
	 * Constructs a CombinedDomainChartBase&lt;T extends Comparable&lt;T&gt;&gt;(implements ICombinedDomainChart&lt;T&gt;) using the given frameTitle(String) and xAxis(ValueAxis):<br/>
	 * - initializes its plot(CombinedDomainXYPlot) with a new CombinedDomainXYPlot using the given xAxis(ValueAxis) as its shared axis.<br/>
	 * - initializes its chart(JFreeChart) with a new JFreeChart based on its plot(CombinedDomainXYPlot).<br/>
	 * - initializes its frame(JFrame) with a new ChartFrame using the given frameTitle(String) as its title and holding its chart(JFreeChart).
	 * @param frameTitle
	 * @param xAxis
	 */
	public CombinedDomainChartBase(String frameTitle, ValueAxis xAxis) {
		plot = new CombinedDomainXYPlot(xAxis);
		chart = new JFreeChart(plot);
		frame = new ChartFrame(frameTitle, chart);
	}
	/**
	 * sets the top left coordinates, width and height of the associated frame(JFrame) with the given x(int), y(int), w(int) and h(int).<br/>
	 * (invokes the <code>frame.setBounds(x, y, w, h)</code> method)
	 * @param x
	 * @param y
	 * @param w
	 * @param h
	 */
	public void setBounds(int x, int y, int w, int h) {
		frame.setBounds(x, y, w, h);
	}
	/**
	 * shows/hides the associated frame(JFrame) depending on the given visible(boolean)
	 * @param visible
	 */
	public void setVisible(boolean visible) {
		frame.setVisible(visible);
	}
	/**
	 * disposes the associated frame(JFrame)<br/>
	 * (invokes the <code>frame.dispose()</code> method)
	 */
	public void dispose() {
		frame.dispose();
	}
	/**
	 * Writes the associated chart(JFreeChart) as a PNG file at the given filename(String) location, using the given w(int) and h(int) as its width and height.
	 * @param filename
	 * @param w
	 * @param h
	 * @throws IOException
	 */
	public void writeToPngFile(String filename, int w, int h) throws IOException {
		FileOutputStream f = new FileOutputStream(filename);
		ChartUtilities.writeChartAsPNG(f, chart, w, h);
		f.close();
	}
	
	/**
	 * Creates new dataset (that will hold all series plotted with the same axis scale).<br/>
	 * An abstract method.
	 * @return dataset.
	 */
	protected abstract XYDataset createDataset();
	
	/**
	 * Creates new series within the dataset (must add it to the dataset).<br/>
	 * An abstract method.
	 * @param dataset dataset that holds the series.
	 * @param legend legend string for this series.
	 * @return series.
	 */
	protected abstract Series createSeries(XYDataset dataset, String legend);
	
	/**
	 * Cleans single series with the respect to the visible window (if this operation is supported). No nothing if you do not want to support this operation.<br/>
	 * An abstract method.
	 * @param series series being cleaned up.
	 * @param max largest value of X-range.
	 */
	protected abstract void cleanSeries(Series series, T max);
	
	/**
	 * Adds new point to the series.<br/>
	 * An abstract method.
	 * @param series series.
	 * @param x x-value of the new point.
	 * @param y y-value of the new point.
	 */
	protected abstract void addToSeries(Series series, T x, double y);
	/**
	 * private final List&lt;Series&gt; seriesList = new ArrayList&lt;Series&gt;();
	 */
	private final List<Series> seriesList = new ArrayList<Series>();
	/**
	 * private final AtomicReference&lt;T&gt; latestX = new AtomicReference&lt;T&gt;();
	 */
	private final AtomicReference<T> latestX = new AtomicReference<T>();
	/**
	 * private final AtomicBoolean isCleanupScheduled = new AtomicBoolean(false);
	 */
	private final AtomicBoolean isCleanupScheduled = new AtomicBoolean(false);
	/**
	 * <strong>1.</strong> If the value of the associated latestX(AtomicReference&lt;T&gt;) is null, or is less than the given x(T):<br/>
	 * -sets the value of the associated latestX(AtomicReference&lt;T&gt;) with the given x(T)<br/>
	 * -if there is no other cleanup scheduled (uses the associated isCleanupScheduled(AtomicBoolean) for this), it invokes the following on the event dispatching thread:<br/>
	 * -Iterates the associated seriesList(List&lt;Series&gt;) and cleans each Series at the current value of the associated latestX(AtomicReference&lt;T&gt;)<br/>
	 *  (invoking the abstract <code>cleanSeries(series, latestX.get())</code> method)<br/>
	 *  <strong>2.</strong> Otherwise it doesn't do anything.
	 * @param x
	 */
	private void cleanupSeries(T x) {
		if(latestX.get() == null || latestX.get().compareTo(x) < 0) {
			latestX.set(x);
			if(isCleanupScheduled.getAndSet(true)) return;
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					for(Series series : seriesList) {
						cleanSeries(series, latestX.get());
					}
					isCleanupScheduled.set(false);
				}
			});
		}
	}
	/**
	 * Creates and returns an IPlotService&lt;T&gt; using the given title(String) for the XYPlot's range axis, the given weight(int) relative to all other plots on the associated plot(CombinedDomainXYPlot)
	 * and the given rangeIncludesZero(boolean) to determine whether the range axis will include 0.<br/>
	 * The job of the IPlotService&lt;T&gt; is to create and add a subplotMain(XYPlot) to the associated plot(CombinedDomainXYPlot).<br/>
	 * Then, every added point to the Series of the subplotMain(XYPlot) is queued and left to the AWT Thread to actually add it, and clear any data that's no longer visible.<br/>
	 * Only one plot should be annotated
	 */
	public IPlotService<T> createPlot(String title, int weight, boolean rangeIncludesZero, boolean annotatedPlot) {
		final XYDataset dataset = createDataset();
//		StandardXYItemRenderer renderer=new StandardXYItemRenderer();
		XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, false);
//		XYToolTipGenerator toolTipGenerator 
//                = StandardXYToolTipGenerator.getTimeSeriesInstance();
//		renderer.setBaseToolTipGenerator(toolTipGenerator);
		XYPlot subplotMain = new XYPlot(dataset, null, new NumberAxis(title), renderer);
		NumberAxis naMain = (NumberAxis) subplotMain.getRangeAxis();
		naMain.setAutoRangeIncludesZero(rangeIncludesZero);
		if(annotatedPlot){
			annotatedXYPlot=subplotMain;
		}
		plot.add(subplotMain, weight);
		
		return new IPlotService<T>() {
			/**
			 * Creates and returns an ISeriesService&lt;T&gt; whose job is to create a Series with the given legend(String) and add it to the associated seriesList(List&lt;Series&gt;).<br/>
			 * Then, every added point to the Series is queued and left to the AWT Thread to actually add it, and clear any data that's no longer visible.
			 */
			public ISeriesService<T> newSeries(String legend) {
				final Series series = createSeries(dataset, legend);
				seriesList.add(series);
				/**
				 * final AtomicBoolean isBusy = new AtomicBoolean(false);
				 */
				final AtomicBoolean isBusy = new AtomicBoolean(false);
				/**
				 * final Queue&lt;Tuple&lt;T,Double&gt;&gt; pendingData = new ConcurrentLinkedQueue&lt;Tuple&lt;T,Double&gt;&gt;();
				 */
				final Queue<Tuple<T,Double>> pendingData = new ConcurrentLinkedQueue<Tuple<T,Double>>();
				
				return new ISeriesService<T>() {
					/**
					 * <strong>1.</strong> If necessary, cleans up each Series in the associated seriesList(List&lt;Series&gt;) and sets the value of the associated latestX(AtomicReference&lt;T&gt;) with the given x(T)<br/>
					 * <strong>2.</strong> Adds the given x(T) and y(double) as a Tuple to the pendingData(Queue&lt;Tuple&lt;T,Double&gt;&gt;) and invokes a Runnable on the AWT Thread that
					 * keeps polling these Tuples and adding them to the Series.
					 * @param x
					 * @param y
					 */
					public void addDataPoint(final T x, final double y) {
						cleanupSeries(x);
						pendingData.offer(new Tuple<T,Double>(x, y));
						if(!isBusy.getAndSet(true)) {
							SwingUtilities.invokeLater(new Runnable() {
								/**
								 * Keeps polling tuple(Tuple&lt;T,Double&gt;) objects from the pendingData(Queue&lt;Tuple&lt;T,Double&gt;&gt;) and adding them to
								 * to the newly created series(Series). Listeners are notified, only once, when the last available tuple(Tuple&lt;T,Double&gt;) is added.<br/>
								 * Uses the abstract <code>void addToSeries(Series series, T x, double y)</code> to add tuples.<br/>
								 * This is run on the event dispatching thread, and uses the isBusy(AtomicBoolean) to make sure there are no concurrent calls.
								 */
								public void run() {
									Tuple<T,Double> tuple = pendingData.poll();
									if(tuple == null) return;
									
									series.setNotify(false);
									
									while(true) {
										Tuple<T,Double> t = pendingData.poll();
										if(t == null) break;
										addToSeries(series, tuple.getObject1(), tuple.getObject2());
										tuple = t;
									}
									
									series.setNotify(true);
									addToSeries(series, tuple.getObject1(), tuple.getObject2());
									
									isBusy.set(false);
								}
							});
						}
					}
				};
			}
		};
	}
	
	final AtomicBoolean isBusy = new AtomicBoolean(false);
	/**
	 * final Queue&lt;XYTextAnnotation;&gt; pendingData = new ConcurrentLinkedQueue&lt;XYTextAnnotation&gt;();
	 */
	final Queue<XYTextAnnotation> pendingData = new ConcurrentLinkedQueue<XYTextAnnotation>();
	
	public void addAnnotation(final XYTextAnnotation annotation){
		if(annotatedXYPlot!=null){
			pendingData.offer(annotation);
			if(!isBusy.getAndSet(true)) {
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						XYTextAnnotation tuple = pendingData.poll();
						if(tuple == null) return;
//						series.setNotify(false);
						while(true) {
							XYTextAnnotation t = pendingData.poll();
							if(t == null) break;
							annotatedXYPlot.addAnnotation(tuple);
							tuple = t;
						}						
//						series.setNotify(true);
						annotatedXYPlot.addAnnotation(tuple);
						isBusy.set(false);
					}
				});
			}
		}
	}
}
